import cv2
import subprocess
import threading
import numpy as np
import time
import os
import queue
import signal
import multiprocessing
import traceback
from queue import Queue, Empty, Full

class PreviewManager:
    def __init__(self, log_callback=None, ffmpeg_path=None):
        self.preview_process = None
        self.preview_thread = None
        self.is_previewing = False
        self.log_callback = log_callback
        self.frame_queue = Queue(maxsize=10)
        self.preview_window_name = "Videopreview"
        self.preview_frame = None
        # 设置FFmpeg路径，默认为系统PATH中的ffmpeg
        self.ffmpeg_path = ffmpeg_path if ffmpeg_path else 'ffmpeg'
    
    def _log(self, message):
        
        """记录日志"""
        if self.log_callback:
            self.log_callback(message)
        else:
            print(message)
    
    def start_device_preview(self, video_device, audio_device=None, width=640, height=480, fps=30):
        """开始设备预览"""
        # 停止现有预览
        self.stop_preview()
        
        # 构建FFmpeg命令用于预览
        input_device = f'video={video_device}'
        
        # 如果提供了音频设备，添加到输入源
        if audio_device:
            input_device = f'video={video_device}:audio={audio_device}'
        
        cmd = [
            self.ffmpeg_path,
            '-f', 'dshow',
            '-i', input_device,
            '-vf', f'scale={width}:{height}:force_original_aspect_ratio=decrease,pad={width}:{height}:(ow-iw)/2:(oh-ih)/2',
            '-pix_fmt', 'bgr24',  # OpenCV使用的格式
            '-r', str(fps),
            '-f', 'rawvideo',
            '-'
        ]
        
        self._log(f"开始设备预览: {video_device}")
        if audio_device:
            self._log(f"音频设备: {audio_device}")
        self._log(f"执行命令: {' '.join(cmd)}")
        
        try:
            # 启动FFmpeg进程
            self.preview_process = subprocess.Popen(
                cmd,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                text=False,
                shell=False  # 使用shell=False更安全
            )
            
            # 启动错误处理线程
            threading.Thread(
                target=self._read_errors,
                daemon=True
            ).start()
            
            self.is_previewing = True
            
            # 启动线程读取视频帧
            self.preview_thread = threading.Thread(
                target=self._read_frames,
                args=(width, height)
            )
            self.preview_thread.daemon = True
            self.preview_thread.start()
            
            # 启动显示线程
            threading.Thread(target=self._show_preview, daemon=True).start()
            
            # 添加短暂延迟确保进程正常启动
            time.sleep(1)
            
            # 检查进程是否仍在运行
            if self.preview_process.poll() is not None:
                self._log("预览进程启动失败，检查设备是否可用")
                self.stop_preview()
                return False
            
            return True
            
        except Exception as e:
            self._log(f"启动预览失败: {str(e)}")
            self.is_previewing = False
            return False
    
    def start_screen_preview(self, screen_index="0", width=640, height=480, fps=30):
        """开始屏幕预览"""
        # 停止现有预览
        self.stop_preview()
        
        # 构建FFmpeg命令用于屏幕预览
        cmd = [
            self.ffmpeg_path,
            '-f', 'gdigrab',
            '-framerate', str(fps),
        ]
        
        # 如果指定了显示器索引，添加monitor_index参数
        if screen_index != "0":
            cmd.extend(["-monitor_index", screen_index])
            
        cmd.extend([
            '-i', 'desktop',
            '-vf', f'scale={width}:{height}',
            '-pix_fmt', 'bgr24',
            '-f', 'rawvideo',
            '-'
        ])
        
        self._log(f"开始屏幕预览: 显示器{screen_index}")
        self._log(f"执行命令: {' '.join(cmd)}")
        
        try:
            # 启动FFmpeg进程
            self.preview_process = subprocess.Popen(
                cmd,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                text=False,
                shell=True
            )
            
            self.is_previewing = True
            
            # 启动线程读取视频帧
            self.preview_thread = threading.Thread(
                target=self._read_frames,
                args=(width, height)
            )
            self.preview_thread.daemon = True
            self.preview_thread.start()
            
            # 启动显示线程
            threading.Thread(target=self._show_preview, daemon=True).start()
            
            return True
            
        except Exception as e:
            self._log(f"启动预览失败: {str(e)}")
            self.is_previewing = False
            return False
    
    def start_region_preview(self, left, top, width=640, height=480, capture_width=800, capture_height=600, fps=30):
        """开始区域预览"""
        # 停止现有预览
        self.stop_preview()
        
        # 构建FFmpeg命令用于区域预览
        cmd = [
            self.ffmpeg_path,
            '-f', 'gdigrab',
            '-framerate', str(fps),
            '-offset_x', str(left),
            '-offset_y', str(top),
            '-video_size', f'{capture_width}x{capture_height}',
            '-i', 'desktop',
            '-vf', f'scale={width}:{height}',
            '-pix_fmt', 'bgr24',
            '-f', 'rawvideo',
            '-']
        
        self._log(f"开始区域预览: {capture_width}x{capture_height} at ({left},{top})")
        self._log(f"执行命令: {' '.join(cmd)}")
        
        try:
            # 启动FFmpeg进程
            self.preview_process = subprocess.Popen(
                cmd,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                text=False,
                shell=True
            )
            
            self.is_previewing = True
            
            # 启动线程读取视频帧
            self.preview_thread = threading.Thread(
                target=self._read_frames,
                args=(width, height)
            )
            self.preview_thread.daemon = True
            self.preview_thread.start()
            
            # 启动显示线程
            threading.Thread(target=self._show_preview, daemon=True).start()
            
            return True
            
        except Exception as e:
            self._log(f"启动预览失败: {str(e)}")
            self.is_previewing = False
            return False
    
    def start_window_preview_dshow(self, window_title, width=640, height=480, fps=30, audio_device=None):
        """使用dshow方式开始窗口预览（支持screen-capture-recorder虚拟设备）"""
        # 停止现有预览
        self.stop_preview()
        
        # 构建FFmpeg命令参数列表，使用这种方式可以更好地处理引号和特殊字符
        cmd = [
            self.ffmpeg_path,
            "-f", "dshow",
            "-video_size", "1920x1080",
            "-i", f"video=screen-capture-recorder:title={window_title}"
        ]
        
        # 添加音频捕获（使用dshow）
        if audio_device:
            audio_input = f'audio="{audio_device}"' if ' ' in audio_device else f'audio={audio_device}'
            cmd.extend(["-f", "dshow", "-i", audio_input])
        
        # 添加视频滤镜和输出设置
        cmd.extend([
            "-vf", f"scale={width}:{height}",
            "-pix_fmt", "bgr24",
            "-f", "rawvideo",
            "-"
        ])
        
        # 构建命令字符串用于日志
        cmd_str = '"' + '" "'.join(cmd) + '"' if ' ' in ''.join(cmd) else ' '.join(cmd)
        
        self._log(f"开始窗口预览(dshow音频): {window_title}")
        self._log(f"执行命令: {cmd_str}")
        
        try:
            # 启动FFmpeg进程
            self.preview_process = subprocess.Popen(
                cmd,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                text=False,
                shell=False  # 使用参数列表时设置为False更安全
            )
            
            self.is_previewing = True
            
            # 启动线程读取视频帧
            self.preview_thread = threading.Thread(
                target=self._read_frames,
                args=(width, height)
            )
            self.preview_thread.daemon = True
            self.preview_thread.start()
            
            # 启动错误读取线程 - 捕获FFmpeg的错误输出
            threading.Thread(target=self._read_errors, daemon=True).start()
            
            # 启动显示线程
            threading.Thread(target=self._show_preview, daemon=True).start()
            
            return True
            
        except Exception as e:
            self._log(f"启动预览失败: {str(e)}")
            self.is_previewing = False
            return False
            
    def start_window_preview(self, window_title, width=640, height=480, fps=30):
        """开始窗口预览"""
        # 停止现有预览
        self.stop_preview()
        
        # 构建FFmpeg命令用于窗口预览 - 添加更多参数以提高兼容性
        cmd = [
            self.ffmpeg_path,
            '-f', 'gdigrab',
            '-framerate', str(fps),
            '-show_region', '1',
            '-draw_mouse', '1',
            '-probesize', '42M',
            '-i', f'title="{window_title}"',
            '-vf', f'scale={width}:{height}',
            '-pix_fmt', 'bgr24',
            '-f', 'rawvideo',
            '-']
        
        self._log(f"开始窗口预览: {window_title}")
        self._log(f"执行命令: {' '.join(cmd)}")
        
        try:
            # 启动FFmpeg进程
            self.preview_process = subprocess.Popen(
                cmd,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                text=False,
                shell=True
            )
            
            self.is_previewing = True
            
            # 启动线程读取视频帧
            self.preview_thread = threading.Thread(
                target=self._read_frames,
                args=(width, height)
            )
            self.preview_thread.daemon = True
            self.preview_thread.start()
            
            # 启动错误读取线程 - 捕获FFmpeg的错误输出
            threading.Thread(target=self._read_errors, daemon=True).start()
            
            # 启动显示线程
            threading.Thread(target=self._show_preview, daemon=True).start()
            
            return True
            
        except Exception as e:
            self._log(f"启动预览失败: {str(e)}")
            self.is_previewing = False
            return False
    
    def preview_file(self, file_path, width=640, height=480, fps=30):
        """预览本地视频文件"""
        # 停止现有预览
        self.stop_preview()
        
        # 构建FFmpeg命令用于文件预览
        cmd = [
            self.ffmpeg_path,
            '-i', file_path,
            '-vf', f'scale={width}:{height}',
            '-pix_fmt', 'bgr24',
            '-r', str(fps),
            '-f', 'rawvideo',
            '-'
        ]
        
        self._log(f"开始文件预览: {file_path}")
        self._log(f"执行命令: {' '.join(cmd)}")
        
        try:
            # 启动FFmpeg进程
            self.preview_process = subprocess.Popen(
                cmd,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                text=False,
                shell=False
            )
            
            # 启动错误处理线程
            threading.Thread(
                target=self._read_errors,
                daemon=True
            ).start()
            
            self.is_previewing = True
            
            # 启动线程读取视频帧
            self.preview_thread = threading.Thread(
                target=self._read_frames,
                args=(width, height)
            )
            self.preview_thread.daemon = True
            self.preview_thread.start()
            
            # 启动显示线程
            threading.Thread(target=self._show_preview, daemon=True).start()
            
            # 添加短暂延迟确保进程正常启动
            time.sleep(1)
            
            # 检查进程是否仍在运行
            if self.preview_process.poll() is not None:
                self._log("预览进程启动失败，检查文件是否存在或格式是否支持")
                self.stop_preview()
                return False
            
            return True
            
        except Exception as e:
            self._log(f"启动文件预览失败: {str(e)}")
            self.stop_preview()
            return False

    
    def _read_frames(self, width, height):
        """读取视频帧的线程函数"""
        frame_size = width * height * 3  # 每个像素3字节(BGR)
        
        try:
            # 短暂延迟确保FFmpeg启动
            time.sleep(1)
            
            while self.is_previewing:
                # 检查进程是否存在且运行中
                if not self.preview_process or self.preview_process.poll() is not None:
                    break
                
                try:
                    # 读取一帧数据
                    if self.preview_process.stdout:
                        frame_data = self.preview_process.stdout.read(frame_size)
                        
                        if len(frame_data) == frame_size:
                            # 转换为numpy数组
                            frame = np.frombuffer(frame_data, dtype=np.uint8).reshape((height, width, 3))
                            
                            # 放入队列
                            if not self.frame_queue.full():
                                self.frame_queue.put(frame, timeout=0.1)
                                self.preview_frame = frame
                except Exception as e:
                    self._log(f"读取帧错误: {str(e)}")
                    break
            
        except Exception as e:
            self._log(f"帧读取线程错误: {str(e)}")
        finally:
            self.is_previewing = False
    
    def _show_preview(self):
        """显示预览的线程函数"""
        try:
            cv2.namedWindow(self.preview_window_name, cv2.WINDOW_NORMAL)
            
            while self.is_previewing:
                try:
                    # 尝试从队列获取帧
                    if not self.frame_queue.empty():
                        frame = self.frame_queue.get(timeout=0.1)
                        self.preview_frame = frame
                        
                        # 显示帧
                        cv2.imshow(self.preview_window_name, frame)
                except queue.Empty:
                    pass  # 队列为空，继续
                except Exception as e:
                    self._log(f"显示帧错误: {str(e)}")
                
                # 处理窗口事件
                key = cv2.waitKey(1) & 0xFF
                if key == 27:  # ESC键
                    self.is_previewing = False
            
        except Exception as e:
            self._log(f"预览显示线程错误: {str(e)}")
        finally:
            # 尝试关闭窗口
            try:
                cv2.destroyWindow(self.preview_window_name)
            except:
                pass
            try:
                cv2.destroyAllWindows()
            except:
                pass
        
        # 函数结束时会自动回到主线程的stop_preview继续处理
    
    def _read_errors(self):
        """读取FFmpeg错误输出"""
        if not self.preview_process:
            return
        
        while self.is_previewing:
            try:
                # 读取错误输出
                error_line = self.preview_process.stderr.readline()
                if error_line:
                    try:
                        # 尝试解码错误信息
                        error_str = error_line.decode('utf-8', errors='ignore').strip()
                        # 过滤掉正常的进度信息，只记录重要错误
                        if error_str and ('error' in error_str.lower() or 'fail' in error_str.lower()):
                            self._log(f"FFmpeg错误: {error_str}")
                    except:
                        pass
            except:
                break
            
            # 添加小延迟避免占用过多CPU
            time.sleep(0.1)
    
    def stop_preview(self):
        """停止预览"""
        try:
            # 设置标志位停止所有循环
            self.is_previewing = False
            
            # 短暂延迟让线程有机会响应停止信号
            time.sleep(0.1)
            
            # 清空帧队列
            while not self.frame_queue.empty():
                try:
                    self.frame_queue.get_nowait()
                except queue.Empty:
                    break
            
            # 终止FFmpeg进程
            if self.preview_process:
                try:
                    self.preview_process.terminate()
                    # 等待进程终止，设置超时
                    try:
                        self.preview_process.wait(timeout=1.0)
                    except subprocess.TimeoutExpired:
                        # 如果超时，强制终止
                        if hasattr(self.preview_process, 'kill'):
                            self.preview_process.kill()
                except Exception as e:
                    self._log(f"终止进程错误: {str(e)}")
                finally:
                    # 关闭标准输出和错误流
                    try:
                        if hasattr(self.preview_process, 'stdout') and self.preview_process.stdout:
                            self.preview_process.stdout.close()
                        if hasattr(self.preview_process, 'stderr') and self.preview_process.stderr:
                            self.preview_process.stderr.close()
                    except:
                        pass
                    self.preview_process = None
            
            # 重置线程引用
            self.preview_thread = None
            
            # 关闭所有OpenCV窗口
            try:
                cv2.destroyWindow(self.preview_window_name)
            except:
                pass
            try:
                cv2.destroyAllWindows()
            except:
                pass
                
        except Exception as e:
            self._log(f"停止预览错误: {str(e)}")
    
    def is_preview_active(self):
        """检查是否正在预览"""
        return self.is_previewing